#include "ircclient.h"
#include "global.h"

#include <QCoreApplication>
#include <QDateTime>
#include <QDir>
#include <QTimer>
#include <QFile>

IrcClient::IrcClient(const ConnectionData& config, QObject *parent) :
    QObject(parent),
    m_socket(nullptr),
    m_connectionData(config),
    m_reconnectReport(false),
    m_connected(false),
    m_triggersIsStopped(false),
    m_shouldAppendMessage(false)
{
    if (m_connectionData.address.isEmpty()) {
        throw std::runtime_error("Empty ConnectionData::address! You trying wrong way to use it, man!");
    }

    QString path {global::toLowerAndNoSpaces(m_connectionData.displayName)};
    QDir dir(m_connectionData.logFolderPath);
    if (not dir.exists()) {
        dir.cdUp();
        dir.mkdir(path);
    }
    dir.cd(path);
    m_connectionData.channels.removeAll("");
    for (auto chan: m_connectionData.channels) {
        chan.remove('#');
        dir.mkdir(chan);
    }
    if (not QFile::exists(dir.path()+global::slash+"about_server.txt")) {
        QFile about(dir.path()+global::slash+"about_server.txt");
        if (about.open(QIODevice::WriteOnly)) {
            about.write("# Server description file.\n"
                        "# HTML is supported. For line breaks, use <br>.\n\n"
                        "<center>¯\\_(ツ)_/¯</center>\n");
            about.close();
        }
        else {
            errorLog("Creating "+dir.path()+global::slash+"about_server.txt is failed");
        }
    }

    connect (&m_pingTimeout,    &QTimer::timeout, this, &IrcClient::pingTimedOut);
    connect (&m_usersActualize, &QTimer::timeout, this, &IrcClient::actualizeUsersList);
    connect (&m_nickRecover,    &QTimer::timeout, this, &IrcClient::nickRecover);
    connect (&m_timerToJoin,    &QTimer::timeout, this, &IrcClient::onLogin);
    connect (&m_stopWriting,    &QTimer::timeout, this, [&](){m_triggersIsStopped = false;});
    m_stopWriting.setSingleShot(true);
}

IrcClient::~IrcClient()
{
    m_socket->write("QUIT IRCaBot " + global::IRCABOT_VERSION.toUtf8() + ": Disconnected by admin");
    m_socket->disconnectFromHost();
}

QStringList IrcClient::getOnlineUsers(const QString &channel)
{
    if (m_online.find(channel) == m_online.end()) {
        return QStringList();
    }
    return m_online[channel];
}

void IrcClient::connectToServer()
{
    if (not m_reconnectReport) {
        emit myNickname(m_connectionData.displayName, m_connectionData.nick);
        emit myOnline(m_connectionData.displayName, m_connected);
        emit startInfo(m_connectionData.displayName, m_connectionData.channels);
    }

    m_socket = new QTcpSocket;
    m_socket->connectToHost(m_connectionData.address, m_connectionData.port);
    if (not m_socket->waitForConnected()) {
        if (not m_reconnectReport) {
            consoleLog("Connection failed. Reconnecting...");
            m_reconnectReport = true;
        }
        QThread::sleep(THREAD_SLEEP_ON_RECONNECT_TIMER);
        connectToServer();
        return;
    }
    if (m_reconnectReport) {
        m_reconnectReport = false;
    }

    connect (m_socket, &QTcpSocket::disconnected, this, &IrcClient::onDisconnected);
    connect (m_socket, &QTcpSocket::readyRead, this, &IrcClient::onRead);

    write("USER " + m_connectionData.user + " . . " + m_connectionData.realName);
    write("NICK " + m_connectionData.nick);
}

void IrcClient::write(const QString &message, bool log)
{
    if (m_socket == nullptr) {
        consoleLog("IrcClient::write() Socket is nullptr!");
        return;
    }

    if (log) {
        consoleLog("<- " + message);
    }

    m_socket->write(message.toUtf8() + '\n');
    if (not m_socket->waitForBytesWritten()) {
        consoleLog("IrcClient::write() Bytes was not written to socket!");
        return;
    }
}

IrcClient::IrcCode IrcClient::getServerCode(const QString &message)
{
    IrcCode code = IrcCode::Failed;
    QString result {message};

    int beginPosition = result.indexOf(' ');
    if (beginPosition == -1) return code;
    result.remove(0, beginPosition+1);
    int endPosition = result.indexOf(' ');
    if (endPosition == -1) return code;
    result.remove(endPosition, result.size()-endPosition);
    bool convert {false};
    int resultInt {result.toInt(&convert)};
    if (not convert) return code;

    switch (resultInt) {
    case 332:
        code = IrcCode::ChannelTopic;
        break;
    case 353:
        code = IrcCode::NamesList;
        break;
    case 366:
        code = IrcCode::EndOfNamesList;
        break;
    case 433:
        code = IrcCode::NickNameIsAlreadyInUse;
        break;
    }

    return code;
}

QString IrcClient::getChannelName(const QString &message)
{
    int begin = message.indexOf('#');
    if (begin == -1) return QString();
    QString result {message};
    result.remove(0,begin);
    int end = result.indexOf(' ');
    if (end == -1) {
        end = result.size();
    }
    else {
        result.remove(end, result.size()-end);
    }
    return result;
}

QString IrcClient::getNickname(const QString &message)
{
    if (not message.startsWith(':')) return QString();
    QString result {message};
    result.remove(0,1);
    result.remove(QRegularExpression("!.*$"));
    if (result.contains(' ')) return QString();
    return result;
}

void IrcClient::toTrigger(const QString& channel, const QString &nickname, const QString &message)
{
    if (m_connectionData.triggers.isEmpty()) return;
    if (m_triggersIsStopped) {
        consoleLog("IrcClient::toTrigger() Trigger request is ignored (anti DDoS)");
        return;
    }
    m_triggersIsStopped = true;
    m_stopWriting.start(WRITING_STOP_TIMER);

    for (auto trigger: m_connectionData.triggers) {
        if (message.contains(m_connectionData.triggers.key(trigger), Qt::CaseInsensitive)) {
            QString response {trigger};
            response.replace(TRIGGER_CHANNEL_FOR_URL_PATTERN,
                             global::toLowerAndNoSpaces(m_connectionData.displayName) + "/" + channel.mid(1, channel.size()-1));
            write("PRIVMSG " + channel + " " + nickname + ", " + response);
            return;
        }
    }
    QString possibleTriggers;
    for (auto trigger: m_connectionData.triggers) {
        possibleTriggers += "'" + m_connectionData.triggers.key(trigger) + "', ";
    }
    possibleTriggers.replace(QRegularExpression(",\\s$"), ".");
    write("PRIVMSG " + channel + " " + nickname + ", try it: " + possibleTriggers);
}

void IrcClient::toChatLog(QString channel, const QString &nick, const QString &message)
{
    QString msg {message}; // Empty message ignored
    msg.remove(" ");
    msg.remove("\t");
    if (msg.isEmpty()) return;

    channel.remove('#');
    emit newMessage(m_connectionData.displayName, channel, nick, message);

    QDir dir(m_connectionData.logFolderPath);
    if (not dir.exists(channel)) {
        dir.mkdir(channel);
    }
    if (not dir.cd(channel)) {
        errorLog("Can't open log folder (1) for #" + channel + " (" + nick + "): " + message);
        return;
    }

    QString year {QDateTime::currentDateTime().toString("yyyy")};
    if (not dir.cd(year)) {
        if (not dir.mkdir(year)) {
            errorLog("Can't create log folder (2) for #" + channel + " (" + nick + "): " + message);
            return;
        }
        if (not dir.cd(year)) {
            errorLog("Can't open log folder (2) for #" + channel + " (" + nick + "): " + message);
            return;
        }
    }

    QString month {QDateTime::currentDateTime().toString("MM")};
    if (not dir.cd(month)) {
        if (not dir.mkdir(month)) {
            errorLog("Can't create log folder (3) for #" + channel + " (" + nick + "): " + message);
            return;
        }
        if (not dir.cd(month)) {
            errorLog("Can't open log folder (3) for #" + channel + " (" + nick + "): " + message);
            return;
        }
    }

    QString day {QDateTime::currentDateTime().toString("dd")};

    QFile file (dir.path() + global::slash + day + ".txt");
    if (not file.open(QIODevice::WriteOnly | QIODevice::Append)) {
        errorLog("Can't open log file for #" + channel + " (" + nick + "): " + message);
        return;
    }

    QString logMessage {"["+nick+"] " + message + '\n'};
    file.write(logMessage.toUtf8());
    file.close();
}

void IrcClient::consoleLog(const QString &message)
{
    qInfo().noquote() << "[" + m_connectionData.displayName + "]" << message;
}

void IrcClient::errorLog(const QString &message)
{
    QFile log(m_connectionData.logFolderPath + global::slash + "error.log");
    consoleLog("[ERROR] " + message);
    if (log.open(QIODevice::WriteOnly | QIODevice::Append)) {
        log.write(QDateTime::currentDateTime().toString().toUtf8() + " " + message.toUtf8() + "\n");
        log.close();
    }
}

void IrcClient::onRead()
{
    m_shouldAppendMessage ?
        m_readingBuffer += m_socket->readAll() :
        m_readingBuffer = m_socket->readAll();

    if (not m_readingBuffer.endsWith('\n'))
    {
        if (not m_shouldAppendMessage) m_shouldAppendMessage = true;
        return;
    }
    if (m_shouldAppendMessage) m_shouldAppendMessage = false;

    m_readingBuffer.remove(QRegularExpression("\n$"));
    m_readingBuffer.remove('\r');
    QStringList messageLines {m_readingBuffer.split('\n')};
    for (auto &line: messageLines) {
//        consoleLog(line);
        process(line);
    }
}

void IrcClient::onLogin()
{
    m_timerToJoin.stop();

    if (not m_connectionData.password.isEmpty()) {
        write("PRIVMSG NICKSERV IDENTIFY " + m_connectionData.password);
    }

    if (m_connectionData.altNick.isEmpty()) {
        write("MODE " + m_connectionData.nick + " +B");
    }
    else {
        write("MODE " + m_connectionData.altNick + " +B");
    }

    for (auto &ch: m_connectionData.channels) {
        write("JOIN " + ch);
    }
}

void IrcClient::onDisconnected()
{
    disconnect (m_socket, &QTcpSocket::readyRead, this, &IrcClient::onRead);
    disconnect (m_socket, &QTcpSocket::disconnected, this, &IrcClient::onDisconnected);
    m_socket->close();
    m_socket->deleteLater();

    m_connected = false;
    emit myOnline(m_connectionData.displayName, m_connected);

    m_pingTimeout.stop();
    m_nickRecover.stop();
    m_timerToJoin.stop();
    m_usersActualize.stop();

    consoleLog("Disconnected from server. Reconnecting...");
    QThread::sleep(THREAD_SLEEP_ON_RECONNECT_TIMER);
    connectToServer();
}

void IrcClient::pingTimedOut()
{
    consoleLog("Ping timed out (361 seconds)");
    if (m_socket != nullptr) {
        m_socket->disconnectFromHost();
    } else {
        consoleLog("Socket already closed");
    }
}

void IrcClient::actualizeUsersList()
{
    consoleLog("Online list actualize...");
    for (auto channel: m_connectionData.channels) {
        write("NAMES " + channel);
    }
}

void IrcClient::nickRecover()
{
    write("NICK " + m_connectionData.nick);
    if (not m_connectionData.password.isEmpty()) {
        write("PRIVMSG NICKSERV IDENTIFY " + m_connectionData.password);
    }
}

void IrcClient::process(const QString &message)
{
    if (m_readingBuffer.startsWith("PING"))
    {
        m_readingBuffer.remove("PING :");
        write("PONG :" + m_readingBuffer, false);
        if (not m_connected)
        {
            consoleLog("Connected to server!");
            m_timerToJoin.start(1000);
            m_usersActualize.start(USER_LIST_ACTIALIZE_TIMER);
            m_connected = true;
            emit myOnline(m_connectionData.displayName, m_connected);
        }
        m_pingTimeout.start(PING_TIMEOUT_TIMER); // 361 secs
        return;
    }

    IrcCode code {getServerCode(message)};
    QString channel {getChannelName(message)};
    QString nickname {getNickname(message)};
    QString raw {message};
    if (code != IrcCode::Failed) {
        if (code == IrcCode::NamesList) {
            raw.remove(QRegularExpression("^.*:"));
            if (m_readNamesList.contains(channel)) {
                m_online[channel] += raw.split(' ');
            }
            else {
                m_online[channel] = raw.split(' ');
                m_readNamesList.push_back(channel);
            }
        }
        else if (code == IrcCode::EndOfNamesList) {
            m_readNamesList.removeAll(channel);
            consoleLog("Online at " + channel + ": " + QString::number(m_online[channel].size()-1));
            emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
        }
        else if (code == IrcCode::ChannelTopic) {
            raw.remove(QRegularExpression("^.*\\s" + channel + "\\s:"));
            consoleLog("Topic at " + channel + ": " + raw);
            emit topicChanged(m_connectionData.displayName, channel, raw);
        }
        else if (code == IrcCode::NickNameIsAlreadyInUse) {
            if (m_connectionData.altNick.isEmpty()) {
                m_connectionData.altNick = m_connectionData.nick + "_" + global::getRandomString(9,3);
                write ("NICK " + m_connectionData.altNick);
                emit myNickname(m_connectionData.displayName, m_connectionData.altNick);

                m_nickRecover.start(NICK_RECOVER_TIMER);
            }
        }
    }
    else {
        // Если нет кода, есть строчное имя действия

        if (m_rgxPrivmsg.match(message).hasMatch()) {
            if (channel.isEmpty()) return; // Private message to bot
            QString userMsg {message};
            userMsg.remove(QRegularExpression("^.*"+channel+"\\s:"));

            if (userMsg.startsWith('.')) {
                userMsg = global::BLINDED_MESSAGE_MERKER;
            }
            else if (userMsg.startsWith("ACTION")) {
                userMsg.remove("ACTION");
                userMsg.remove("");
                userMsg = "*** " + userMsg + " ***";
            }

            consoleLog(channel + " (" + nickname + "): " + userMsg);

            QString myCurrentNick;
            if (m_connectionData.altNick.isEmpty()) {
                myCurrentNick = m_connectionData.nick;
            }
            else {
                myCurrentNick = m_connectionData.altNick;
            }

            if (QRegularExpression("^"+myCurrentNick+"(:|,|!).*").match(userMsg).hasMatch()) {
                userMsg.remove(0, myCurrentNick.size());
                toTrigger(channel, nickname, userMsg);
            }
            else {
                toChatLog(channel, nickname, userMsg);
            }
        }
        else if (m_rgxJoin.match(message).hasMatch()) {
            if (nickname == m_connectionData.nick or nickname == m_connectionData.altNick) {
                consoleLog("I joined to " + channel);
                return;
            }
            m_online[channel].push_back(nickname);
            consoleLog("JOIN " + getNickname(message) + " to " + channel + ". "
                       "Online: " + QString::number(m_online[channel].size()-1));

            emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
        }
        else if (m_rgxPart.match(message).hasMatch()) {
            const std::array<char,4> specSymbols {'+', '&', '~', '@'};
            for (const auto& sym: specSymbols) {
               m_online[channel].removeAll(sym+nickname);
            }
            m_online[channel].removeAll(nickname);
            consoleLog("PART " + getNickname(message) + " from " + channel + ". "
                       "Online: " + QString::number(m_online[channel].size()-1));

            emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
        }
        else if (m_rgxKick.match(message).hasMatch()) {
            QString kickedUser {message};
            kickedUser.remove(QRegularExpression("^.*"+channel+"\\s"));
            kickedUser.remove(QRegularExpression("\\s.*$"));
            m_online[channel].removeAll(kickedUser);
            consoleLog("KICK " + nickname + " from " + channel + ". "
                       "Online: " + QString::number(m_online[channel].size()-1));

            emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
        }
        else if (m_rgxMode.match(message).hasMatch()) {
            if (QRegularExpression("^.*\\sMODE\\s"+channel+"\\s(\\+|-)b").match(message).hasMatch()) {
                write("NAMES "+channel);
            }
        }
        else if (m_rgxQuit.match(message).hasMatch()) {
            const std::array<char, 5> prefixes {'~' /*owner*/,    '&' /*admin*/,
                                                '@' /*operator*/, '%' /*half-op*/,
                                                '+' /*voiced*/};
            for (auto &ch: m_online) {
                ch.second.removeAll(nickname);
                for (const auto& p: prefixes) {
                    ch.second.removeAll(p+nickname);
                }
                emit userOnline(m_connectionData.displayName, ch.first, ch.second);
            }
            consoleLog("QUIT " + nickname);
        }
        else if (m_rgxNick.match(message).hasMatch()) {
            if (message.contains(":" + m_connectionData.altNick + " NICK :" + m_connectionData.nick)) {
                // Успешная смена никнейма. Синтаксис ответа сервера ":старый_ник NICK :новый_ник"
                m_nickRecover.stop();
                m_connectionData.altNick.clear();
                emit myNickname(m_connectionData.displayName, m_connectionData.nick);
                consoleLog("Default nickname (" + m_connectionData.nick + ") is recovered!");
            }
            else {
                QString oldNick {getNickname(message)};
                QString newNick {message};
                newNick.remove(QRegularExpression("^.*NICK :"));
                for (auto &ch: m_online) {
                    for (auto &nick: ch.second) {
                        if (nick == oldNick) nick = newNick;
                    }
                }
                if (oldNick.isEmpty()) {
                    consoleLog("I was renamed to " + newNick + " (!)");
                    if (newNick != m_connectionData.nick) {
                        m_connectionData.altNick = newNick;
                        emit myNickname(m_connectionData.displayName, m_connectionData.altNick);
                        m_nickRecover.start(NICK_RECOVER_TIMER);
                    }
                    else {
                        emit myNickname(m_connectionData.displayName, m_connectionData.nick);
                    }
                }
                else {
                    consoleLog("NICK " + oldNick + " renamed to " + newNick);
                    emit userOnline(m_connectionData.displayName, channel, m_online[channel]);
                }
            }
        }
        else if (m_rgxTopic.match(message).hasMatch()) {
            raw.remove(QRegularExpression("^.*\\s" + channel + "\\s:"));
            consoleLog("Topic at " + channel + ": " + raw);
            emit topicChanged(m_connectionData.displayName, channel, raw);
        }
    }

    if (message.startsWith("ERROR")) {
        errorLog(message);
    }
}
